<?php declare(strict_types=1);
/**
 * @author      xianganyall <xianganyall@gmail.com>
 * @copyright   2023-2025 owner
 **/

namespace Srv\Libs\Storage;

use Redis;
use RedisException;
use Srv\Libs\Common\CommJson;
use Srv\Libs\Common\CommValid;
use Srv\Libs\ConfModel\CacheConf;
use Srv\Libs\Exception\RedisConnectException;
use Srv\Libs\Frame\Conf;
use Srv\Libs\Pools\PoolCache;
use Srv\Libs\Pools\PoolItem;

class DataRedis extends DataStorageAbstract
{
    private ?PoolCache $PoolCache           = null;                 // 缓存对象集合
    private ?PoolItem $PoolItem             = null;                 // 对象Item单个
    private ?Redis $Redis                   = null;                 // Redis对象
    private string $serverPass              = '';                   // PASS
    private string $prefix                  = '';                   // 前缀Key
    private bool $hostIsSock                = false;                // HOST是否是Unix
    private string $serverHost              = '127.0.0.1';          // HOST
    private int $serverPort                 = 6379;                 // PORT
    private int $serverTimeOut              = 2;                    // 连接超时时间[秒]
    private int $serverDb                   = -1;                   // 连接数据库Id,默认为-1不选择数据库
    private bool $isKeepAlive               = false;                // 是否启用长连接
    private string $persistentId            = '';                   // 连接名

    /**
     * DataRedis constructor.
     * @param CacheConf $CacheConf
     * @param string $persistentId
     */
    public function __construct(CacheConf $CacheConf, string $persistentId)
    {
        $server                 = trim($CacheConf->getHost());
        $port                   = $CacheConf->getPort();
        if(CommValid::isUnix($server)){     // unix:///srv/conf/redis/redis.sock
            $server             = substr($server, 7);
            $port               = 0;
            $this->hostIsSock   = true;
        }else{
            $port               = min(max($port, 0), 65535);
            $this->hostIsSock   = false;
        }
        $this->serverHost       = $server;
        $this->serverPort       = $port;
        $this->serverPass       = $CacheConf->getPassword();
        $this->prefix           = $CacheConf->getPrefix();
        $this->serverDb         = $CacheConf->getDb();
        $this->isKeepAlive      = $CacheConf->isKeepalive();
        $this->persistentId     = $persistentId;
    }

    /**
     * @param string $groupName
     * @param string $persistentId
     * @param string $tagName
     * @return static
     * tag
     */
    final public static function tag(string $groupName, string $persistentId, string $tagName = 'auto'):self
    {
        $thisObject             = static::tagGet($tagName, [Conf::getCacheConfGroup()->getByGroupName($groupName), $persistentId]);
        $thisObject->connect();
        return $thisObject;
    }

    /**
     * __destruct
     */
    public function __destruct()
    {
        $this->close();
    }

    /**
     * @return bool
     * isUnixHost
     */
    public function isUnixHost():bool
    {
        return $this->hostIsSock;
    }

    /**
     * @param PoolCache $PoolCache
     * @param PoolItem $PoolItem
     * @return bool
     * 初始化连接池
     */
    public function initPool(PoolCache $PoolCache, PoolItem $PoolItem):bool
    {
        if($this->PoolCache instanceof PoolCache || $this->PoolItem instanceof PoolItem) return false;
        $this->PoolCache    = $PoolCache;
        $this->PoolItem     = $PoolItem;
        return true;
    }

    /**
     * @return bool
     * 释放连接池
     */
    public function freePool():bool
    {
        return $this->PoolCache instanceof PoolCache && $this->PoolItem instanceof PoolItem && $this->PoolCache->freePoolItem($this->PoolItem);
    }

    /**
     * @return bool
     * 是否是在使用
     */
    public function isUse():bool
    {
        return ($this->PoolCache instanceof PoolCache && $this->PoolItem instanceof PoolItem && $this->PoolItem->isUse());
    }

    /**
     * @return bool
     * 是否已连接
     */
    public function isConnected():bool
    {
        if($this->Redis instanceof Redis){
            try{
                return $this->Redis->ping('+PONG') === '+PONG';
            }catch (RedisException $RedisException){
                return false;
            }
        }
        return false;
    }

    /**
     * @throws RedisConnectException
     * 自动判断并连接
     */
    public function connect():bool
    {
        if(strlen($this->serverHost) < 1){
            throw new RedisConnectException(CommJson::encodeArray(['msg' => 'Configure error', 'host' => strlen($this->serverHost) > 0]), -1);
        }
        if($this->isConnected()) return true;
        $this->Redis        = new Redis();
        try{
            if($this->isKeepAlive){
                $connect    = $this->Redis->pconnect($this->serverHost, $this->serverPort, $this->serverTimeOut, $this->persistentId);
            }else{
                $connect    = $this->Redis->connect($this->serverHost, $this->serverPort, $this->serverTimeOut, $this->persistentId);
            }
        }catch(RedisException $RedisException){
            $this->Redis    = null;
            throw new RedisConnectException(CommJson::encodeArray(['msg' => 'Connection failed, '.$RedisException->getMessage(), 'host' => strlen($this->serverHost) > 0, 'port' => $this->serverPort, 'pId' => $this->persistentId]), -1);
        }
        if(!$connect) throw new RedisConnectException(CommJson::encodeArray(['msg' => 'Connection failed', 'host' => strlen($this->serverHost) > 0, 'port' => $this->serverPort, 'pId' => $this->persistentId]), -1);
        if(strlen($this->serverPass) > 0 && !$this->Redis->auth($this->serverPass)){
            throw new RedisConnectException(CommJson::encodeArray(['msg' => 'Auth failed', 'pass' => strlen($this->serverPass) > 0]), -1);
        }
        if($this->serverDb > -1 && !$this->Redis->select($this->serverDb)){
            throw new RedisConnectException(CommJson::encodeArray(['msg' => 'Select failed', 'db' => $this->serverDb]), -1);
        }
        $this->Redis->setOption(Redis::OPT_SERIALIZER, strval(Redis::SERIALIZER_NONE)); // Redis::SERIALIZER_NONE
        return true;
    }

    /**
     * @return bool
     * 关闭连接
     */
    public function close():bool
    {
        if(!is_null($this->Redis)) return $this->Redis->close();
        return true;
    }

    /**
     * @return Redis
     * getRedis
     */
    public function getRedis():Redis
    {
        return $this->Redis;
    }

    /**
     * @param string $key
     * @return string
     * 获取Key名
     */
    public function getKeyName(string $key):string
    {
        return $this->prefix.$key;
    }

    /**
     * @param string $key
     * @return int
     * getTtl
     */
    public function getTtl(string $key):int
    {
        return $this->Redis->ttl($this->getKeyName($key));
    }

    /**
     * @param string $key
     * @param int $expire
     * @return bool
     * 设置有效期
     */
    public function setExpire(string $key, int $expire):bool
    {
        return $this->Redis->expire($this->getKeyName($key), $expire);
    }

    /**
     * @param string $key
     * @param string $val
     * @param int $expire
     * @return bool
     * 设置Key有效期(Key必须不存在)
     */
    public function setNx(string $key, string $val, int $expire):bool
    {
        return $this->Redis->set($this->getKeyName($key), $val, ['nx', 'ex' => $expire]);
    }

    /**
     * @param string $key
     * @param string $val
     * @param int $expire
     * @return bool
     * 设置Key有效期(Key必须存在)
     */
    public function setXx(string $key, string $val, int $expire):bool
    {
        return $this->Redis->set($this->getKeyName($key), $val, ['xx', 'ex' => $expire]);
    }

    /**
     * @param string $key
     * @param string $val
     * @param int $expire
     * @return bool
     * 设置变量值
     */
    public function set(string $key, string $val, int $expire = 0):bool
    {
        $expire     = min(max($expire, 0), 2592000);    // 0-永久,或30天内倒计时
        if($expire <= 0){
            return $this->Redis->set($this->getKeyName($key), $val);
        }else{
            return $this->Redis->setex($this->getKeyName($key), $expire, $val);
        }
    }

    /**
     * @param string $key
     * @return float
     * getFloat
     */
    public function getFloat(string $key):float
    {
        $cacheData      = $this->get($key);
        if(is_float($cacheData)) return $cacheData;
        if(is_string($cacheData) || is_int($cacheData)) return floatval($cacheData);
        return 0.0;
    }

    /**
     * @param string $key
     * @return int
     * getInt
     */
    public function getInt(string $key):int
    {
        $cacheData      = $this->get($key);
        if(is_int($cacheData)) return $cacheData;
        if(is_string($cacheData)) return intval($cacheData);
        return 0;
    }

    /**
     * @param string $key
     * @return string
     * getStr
     */
    public function getStr(string $key):string
    {
        $cacheData  = $this->get($key);
        return is_string($cacheData) ? $cacheData : '';
    }

    /**
     * @param string $key
     * @return array
     * getAry
     */
    public function getAry(string $key):array
    {
        $cacheData  = $this->get($key);
        if(!is_string($cacheData)) return [];
        return CommJson::decodeArray($cacheData);
    }

    /**
     * @param string $key
     * @return bool|mixed|string
     * 获取变量值
     */
    public function get(string $key)
    {
        return $this->Redis->get($this->getKeyName($key));
    }

    /**
     * @param string $key
     * @return bool
     * 删除变量
     */
    public function del(string $key):bool
    {
        if($this->Redis->del($this->getKeyName($key))) return true;
        return $this->Redis->get($this->getKeyName($key)) === false;
    }

    /**
     * @param string $key
     * @param int $offset
     * @return int
     * 自增变量Int值
     */
    public function inc(string $key, int $offset = 1):int
    {
        return $offset <= 1 ? $this->Redis->incr($this->getKeyName($key)) : $this->Redis->incrBy($this->getKeyName($key), $offset);
    }

    /**
     * @param string $key
     * @param int $offset
     * @return int
     * 自减变量Int值
     */
    public function dec(string $key, int $offset = 1):int
    {
        return $offset <= 1 ? $this->Redis->decr($this->getKeyName($key)) : $this->Redis->decrBy($this->getKeyName($key), $offset);
    }

    /**
     * @return array
     * 获取状态
     */
    public function getStatus():array
    {
        $infoData   = $this->Redis->info();
        if(!is_array($infoData)) $infoData = [];
        return $infoData;
    }

    /**
     * @return string
     * 获取版本
     */
    public function getVersion():string
    {
        $status     = $this->getStatus();
        return $status['redis_version'] ?? '';
    }

    /**
     * @param array $keyList
     * @return int
     * Key存在的个数
     */
    public function existsKeyNum(array $keyList):int
    {
        if(count($keyList) < 1) return 0;
        $result = $this->Redis->exists(array_map(function($key){ return $this->getKeyName($key); }, $keyList));
        return is_int($result) ? $result : 0;
    }

    /**
     * @param string $key
     * @param mixed ...$value
     * @return bool|int
     * 左侧队列入站
     */
    public function lPush(string $key, ...$value)
    {
        return $this->Redis->lPush($this->getKeyName($key), ...$value);
    }

    /**
     * @param string $key
     * @return bool|mixed
     * 左侧队列出站
     */
    public function lPop(string $key)
    {
        return $this->Redis->lPop($this->getKeyName($key));
    }

    /**
     * @param string $key
     * @param mixed ...$value
     * @return bool|int
     * 右侧队列入站
     */
    public function rPush(string $key, ...$value)
    {
        return $this->Redis->rPush($this->getKeyName($key), ...$value);
    }

    /**
     * @param string $key
     * @return bool|mixed
     * 右侧队列出站
     */
    public function rPop(string $key)
    {
        return $this->Redis->rPop($this->getKeyName($key));
    }

    /**
     * @param string $key
     * @return int
     * 获取数据长度
     */
    public function lLen(string $key):int
    {
        $length     = $this->Redis->lLen($this->getKeyName($key));
        return $length === false ? -1 : $length;
    }
}